[TOC]
1 引言
在前面高性能代码优化规则参考
一文中,我们举了一个优化的例子;但是,但是,结果优化变成劣化(恶化了),演砸了。我们继续看看到底是怎么回事?
2 优化规则评价
先复原一下场景。
光说不练假把式,每种语言运行环境都有各自的表达方式,这里用“最简单“的JavaScript做一个演示。
1 | var i,len,testData = [],testCount = 9999,repeatCount = 999; |
多次执行上面的代码查看效果,结果如下:
在Safari 10.1 (12603.1.30.0.34)
下的运行结果(基本稳定):
1 | origin: 163.225ms |
在Chrome 58.0.3029.96
下的运行结果(不太稳定):
1 | origin: 81.718017578125ms |
在NodeJS 7.9.0
下的运行结果(不太稳定):
1 | origin: 110.480ms |
上面结果看来,优化循环判断条件和数据缓存都有一定的效果;但最后一种风骚的写法,看起来少了一个局部变量,但执行结果是成倍的劣化(恶化)。
这曾经也是一种代码优化的方式,作者也深中其毒,但这已经不那么重要了。现在问题来了,为什么会这么劣化,是一个值得研究的问题。
3 问题深入研究
3.1 找工具
- 搜索
v8 perf tool
Google 第一条得到一个工具列表 - 筛选掉其中内存和CPU相关的,剩下
irhydra
可以做中间代码分析的 - 我们就是想看看咱们的源码被转成什么样的代码来执行了
3.2 搞出来看一看
- node 本身是基于v8的,所以也不用各种下载v8源码编译什么的了,直接上
- 使用node 生成
irhydra
需要的内容 - 将上面的源代码存为
perf-opt.js
- 执行下面的命令
1 | node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --print-opt-code perf-opt.js |
- 在
IRHydra2
的界面导入上面生成的.asm
和.cfg
文件 - 可以看到我们四个函数分别对应的中间代码了
3.3 找文档 捋主线
- 搜索
v8 intermediate representations
在得到的结果中可以得到如下的概念Crankshaft
是v8的编译优化器Hydrogen
是Crankshaft
中的HIR(High-Level intermediate representations)中间代码,更接近于源代码,跟机器无关Lithium
是Crankshaft
中的LIR(Low-Level intermediate representations)中间代码,更接近于机器码,跟机器相关SSA(Static Single Assignment)
是Hydrogen
的描述格式,是一种组织IR(intermediate representations)的方式。
- 搜索
Static Single Assignment
可以得到一些关于SSA的参考 - 参考资料
- 《Compiled Compiler Templates for V8》
- 《Static Single Assignment Book》
- 没有找到更详尽的关于SSA的文档,比如SSA的指令及其含义说明,只有v8源码中的
src/crankshaft/hydrogen-instructions
,如果你找到了,请不吝赐教(怎么找的?)。
3.4 中间代码分析
3.4.1 工具介绍
Load Compilation Atrifacts
加载.asm
和.cfg
文件IR
标签页,打开眼睛可以看到源代码和中间代码的映射关系,深红色的竖条表示循环范围Graph
标签页,打开问号可以查看控制流图的说明,点击图块可以跳转到IR
标签页
3.4.2 总体结果
- 前三段代码(origin,optLen,optIdx)的控制流图完全一致,B3-B8是循环内容
- 第三段代码(optCmp)与其他三段差别较大,
3.4.3 详细对比
3.4.3.1 origin 与 optLen的区别
- origin比optLen在B2块少一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i33
,在循环内容之外 - origin比optLen在B3块多一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i42
,每次循环都有这个操作 - origin比optLen在B5块少一个
LoadNamedField t2.%length@24 Smi
获取数组长度,代号i62
,每次循环都有,这个地方不科学(mark),需要用i62
的地方直接使用i33
就ok - 其他操作无差异
3.4.3.2 optLen与optIdx的区别
- optLen比optIdx在B5块多一个
var[4] = t106
赋值,但这个赋值后面好像没用上。。。
3.4.3.3 optCmp与optLen比较的异常
- 获取data[i]的时候多了
CheckMaps t2 [0x12044e7ace69](stability-check)
操作 - 存储乘积结果的时候多了
CallWithDescriptor t76 t7 t16 t86 t87 s88 t74 #0 changes[*] Tagged
- 异常的描述(这是代码未被编译器优化识别的警告信息)
1 | This instruction has side effects **unknown** to the compiler. |
3.5 看起来还没有找到想要的
- 上面的
SSA
本身存在问题,参考意义不大 - 没有比较出前面三个代码之间的差异
- 最后一个优化搞砸了还是没解释清楚
4 重头再来
4.1 使用PHP来实现(为什么?)
- 在这个例子上,PHP跟JavaScript极其相似
- PHP是世界上最好的语言(别打脸~~)
- PHP下用
VLD
查看opcode
我玩过一次,比SSA
看起来更细致
1 | <?php |
4.2 执行上面的PHP代码
- 将上面的内容存为
perf-opt.php
- 运行
php -f perf-opt.php
- 在
PHP 5.6.30
上面的执行结果(稳定)
1 | origin:4.74019000 |
4.3 也可以使用node自带的工具来看
- 执行命令
1 | node --prof perf-opt.js |
- 转换结果
1 | node --prof-process isolate-xxx-v8.log > processed.log |
- 查看
processed.log
5 结论
- 某些优化规则在不同的编译环境下,不一定能得到一致的效果
- 规则是死的,但编译系统不断在改变和发展
- 有可能我们的优化规则,跟编译器的优化冲突了,成了执行优化的障碍
6 参考资料
《Compiled Compiler Templates for V8》
《Static Single Assignment Book》
[WebKit] JavaScriptCore解析–高级篇(一) SSA (static single assignment)
a closer look at crankshaft, v8’s optimizing compiler
最后更新: 2022年03月02日 03:32
原始链接: http://rawbin-.github.io/performance/2017-05-07-code-optmization-ext/